import asyncio
import json
import sqlite3
import traceback
from sqlite3 import OperationalError
from typing import NamedTuple, Iterable

from aiogram import Bot

from aiogram.dispatcher import Dispatcher, FSMContext
from aiogram.dispatcher.filters import BoundFilter, AdminFilter, Command
from aiogram.dispatcher.handler import SkipHandler, CancelHandler
from aiogram.dispatcher.middlewares import BaseMiddleware
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, Update, ChatPermissions, BotCommand, \
    Chat, BotCommandScopeChat, \
    BotCommandScopeAllChatAdministrators
from aiogram.utils import executor
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram import types
from aiogram.utils.exceptions import ChatNotFound, MessageToDeleteNotFound, Unauthorized, MessageCantBeDeleted
from aiogram.utils.markdown import hlink, hbold, hpre, hitalic, underline, hunderline
from loguru import logger

from sqliter import Database
import config

bot = Bot(token=config.bot_token, parse_mode='HTML')
dp = Dispatcher(bot, storage=MemoryStorage())
db = Database('database.db')

logger.add('bot.log', format="{time:DD-MM-YYYY HH:mm:ss} | {level} | {message}",
           rotation='1MB', compression='zip', enqueue=True)

filter_worlds = [word[1] for word in db.select_all_word()]
chats = {}


async def update_chat_list():
    '''
    :return: dict{chat_id: [require_channel_id]}
    '''
    new_chats = {}
    chats_db = db.select_all_chats()
    for i in chats_db:
        new_chats[i[0]] = []
    for i in chats_db:
        new_chats[i[0]].append(i[1])
    return new_chats


async def check_subs(m: types.Message, user_id=None):
    if user_id is None:
        user_id = m.from_user.id
    try:
        req_channels = chats[m.chat.id]
    except KeyError:
        return 2
    subscribe_channels = []
    for channel in req_channels:
        res = await dp.bot.get_chat_member(channel, user_id)
        if res.status == 'left':
            return 1
        elif res.status == 'member':
            subscribe_channels.append(channel)
    if len(req_channels) == len(subscribe_channels):
        return 0


async def subs_keys(chat_id, user_id) -> InlineKeyboardMarkup:
    global chats
    chats_entity = [await dp.bot.get_chat(c) for c in chats[chat_id]]
    chats_button = [(chat.title, chat.invite_link) for chat in chats_entity]
    keyboard = InlineKeyboardMarkup(row_width=1)
    keyboard.add(*[InlineKeyboardButton(text=n, url=url) for n, url in chats_button])
    keyboard.add(*[InlineKeyboardButton(text=n, callback_data=c) for n, c in
                   {'✅ Проверить подписку': json.dumps({'subs': user_id})}.items()])
    return keyboard


class JoinUsers(BaseMiddleware):
    async def on_pre_process_update(self, update: Update, data: dict):
        global chats
        try:
            if update.my_chat_member.new_chat_member.status == "administrator":
                if update.my_chat_member.chat.id not in chats:
                    db.add_channel(0, update.my_chat_member.chat.id)
        except:
            pass

        try:
            if update.message.new_chat_members:
                await welcome_message(update.message)
                raise CancelHandler
        except Exception:
            return


class IsAdmin(BoundFilter):
    async def check(self, m: types.Message) -> bool:
        if m.from_user.id in config.ADMIN_ID:
            return True


async def welcome_message(m: types.Message):
    await dp.bot.delete_message(m.chat.id, m.message_id)
    user_id = m.from_user.id
    chat_id = m.chat.id
    if chat_id in chats:
        text = f'Привет, {hlink(m.from_user.full_name, f"tg://user?id={user_id}")}\n' \
               f'Чтобы начать общение в этом чате подпишись по ссылкам ниже и нажми кнопку чтобы проверить подписку\n'
        text += '\nНа подписку у тебя еcть 10 минут'
        keyboard = await    subs_keys(chat_id, user_id)
        mess = await dp.bot.send_message(m.chat.id, text, reply_markup=keyboard)
        await asyncio.sleep(10 * 60)
        await mess.delete()
    logger.info(f'New User: {m.from_user.full_name}(id: {m.from_user.id})')


@dp.callback_query_handler()
async def check_subscribe_button(c: types.CallbackQuery):
    global chats
    m = c.message
    data = json.loads(c.data)
    if c.from_user.id not in [data['subs']]:
        await c.answer(f'Нажми на кнопку под сообщением, где упоминают именно тебя\n', show_alert=True)
        return
    result = await check_subs(m, data['subs'])
    if result == 0:
        await c.answer('Умничка, теперь твои сообщения не будут удаляться. Пиши на здоровье', show_alert=True)
        try:
            await m.delete()
        except MessageToDeleteNotFound:
            pass
    elif result == 1:
        await c.answer("Ты не на всё подписался", show_alert=True)


@dp.message_handler(commands=['help'])
async def help_message(m: types.Message):
    text = f'Чтобы добавить канал или группу в список обязательных для подписки нужно:\n' \
           f'1. Добавить бота как админа в этот канал или группу.\n' \
           f'2. Переслать сообщение из канала или группы в группу где этот канал или ' \
           f'группа должны быть обязательными к подписке\n' \
           f'{hitalic("При пересылке сообщения из группы есть ньюанс.")} Надо пересылать сообщение от имени группы.\n' \
           f'Такие сообщения может отправлять админ с включенной анонимностью.\n' \
           f'3. Ответить на пересланное сообщение командой <code>/channel_add</code> \n' \
           f'4. Готово! Канал/Группа теперь обязательна для подписки в этом чате.\n' \
           f'\tP.S. По такой е схеме группа или канал удаляется из списка обязательных, ' \
           f'а именно, ответом на пересланное сообщение из нужного канала или чата, только ' \
           f'с коммандой <code>/channel_del</code>\n' \
           f'\tP.P.S. Кликнув по коммандам выше, они скопируются.\n\n' \
           f'/channel_list - покажет все обязательные каналы для этого чата' \
           f'{hbold("Работа с стоп словами:")}\n' \
           f'В любом чате где есть бот или в личном диалоге с ним напишите <code>/stop_add </code>[нужное вам слово]\n' \
           f'Например так: /stop_add бинанс\n' \
           f'Теперь все сообщения со словом "бинанс" будут удаляться.\n' \
           f'Удаляются слова командой <code>/stop_del </code>\n' \
           f'/stop_list - Покажет список всех стоп слов\n'

    await m.answer(text)


@dp.message_handler(AdminFilter(), commands=['channel_add'])
async def channel_add(m: types.Message):
    global chats
    try:
        t = chats[m.chat.id]
    except KeyError:
        chats[m.chat.id] = []

    try:
        if m.reply_to_message.forward_from_chat.type in ['channel', 'supergroup']:

            if m.reply_to_message.forward_from_chat.id in chats[m.chat.id]:
                exist = await m.reply('Этот канал уже есть в списке обязательных\n'
                                      'Через 10 секунд это сообщение удалится')
                await asyncio.sleep(10)
                await exist.delete()
                await m.delete()
                return 0

            try:
                cha = await dp.bot.get_chat(m.reply_to_message.forward_from_chat.id)
                db.add_channel(m.chat.id, m.reply_to_message.forward_from_chat.id)
                chats = await update_chat_list()
                logger.success(f'Добавил в список обязательных каналов {m.reply_to_message.forward_from_chat.title} '
                               f'в чате{m.chat.title}')
                mess = await m.reply('Добавил в список обязательных каналов\n'
                                     'Через 10 секунд это сообщение удалится')
                await asyncio.sleep(10)
                await m.delete()
                await mess.delete()
            except Unauthorized:
                logger.error(f"[{m.chat.title}] Бот не является админом в канале: "
                             f"{m.reply_to_message.forward_from_chat.title} "
                             f"id:{m.reply_to_message.forward_from_chat.id}")
                reply = await m.answer('Бот не является админом в этом канале!')
                await asyncio.sleep(10)
                await reply.delete()
                await m.delete()
                return 0
    except MessageToDeleteNotFound:
        pass
    except MessageCantBeDeleted as e:
        logger.error(f'[{m.chat.title}] {e}')
    except AttributeError:
        pass


@dp.message_handler(AdminFilter(), commands=['channel_del'])
async def channel_del(m: types.Message):
    global chats
    try:
        if m.reply_to_message.forward_from_chat.type in ['channel', 'supergroup']:
            db.delete_channel(m.chat.id, m.reply_to_message.forward_from_chat.id)
            chats = await update_chat_list()
            logger.success(
                f'Канал {m.reply_to_message.forward_from_chat.title} успешно удалён из обязательных в чате {m.chat.title}')
            text = 'Канал успешно удалён'
            text += '\n\nСообщение удалится через 20 секунд'
            mess = await m.reply(text)
            await asyncio.sleep(20)
            await m.delete()
            await mess.delete()
    except MessageCantBeDeleted as e:
        logger.error(f'{e}\n{json.loads(m)}')
    except AttributeError:
        pass
    except OperationalError as e:
        logger.error(f"[channel_del] {e}")
        await asyncio.sleep(10)
        await channel_del(m)


@dp.message_handler(AdminFilter(), commands=['channel_list'])
async def channel_list(m: types.Message):
    channels_id = [c[1] for c in db.select_channels(m.chat.id)]
    channels_entity = []

    for c in channels_id:
        try:
            entity = await dp.bot.get_chat(c)
            channels_entity.append(entity)
        except ChatNotFound:
            continue
        except Unauthorized:
            text = f"chat_id:{c}\n Похоже, что бот не является админом канала"
            channels_entity.append(Chat(id=c, title=text, invite_link='not_found'))
    chats_formated = "\n".join([f"🔸\t{hlink(chat.title, chat.invite_link)}" for chat in channels_entity])

    text = f'Список обязательных каналов для этого чата: \n' \
           f'{chats_formated}' if len(channels_entity) > 0 else 'Здесь нет обязательных каналов для подписки'
    text += '\n\nСообщение удалится через 20 секунд'
    mess = await m.reply(text)
    await asyncio.sleep(20)
    await m.delete()
    await mess.delete()


@dp.message_handler(IsAdmin(), commands=['channel_add', 'channel_del', 'channel_list'])
async def err_message(m: types.Message):
    logger.error(f'Этот {m.from_user.full_name}, опять мне личку пишет.....')
    await m.answer('Эту команду используй в групповом чате.')


async def collect_words(m: types.Message):
    lines = m.text.split('\n')
    words = []

    # если одна строка и после команды что-то написанно
    if len(lines) == 1 and len(lines[0].split()) > 1:
        # склеиваем слова после команды и добавляем в общий список
        word = ' '.join([i.strip() for i in lines[0].split()[1::]])
        words.append(word)

    # Если строк больше
    elif len(lines) > 1:
        # В первой строчке не только команда
        if len(lines[0].split()) > 1:
            word = ' '.join([i.strip() for i in lines[0].split()[1::]])
            words.append(word)
        for line in lines[1::]:
            words.append(line.strip())
    return words


@logger.catch
@dp.message_handler(IsAdmin(), commands=['stop_add'])
@dp.message_handler(AdminFilter(), commands=['stop_add'])
async def add_world(m: types.Message):
    words = await collect_words(m)
    processed_words = []

    for w in words:
        try:
            db.add_word(w)
            processed_words.append(f"✅ {w} - добавленно в список стоп слов")
            logger.success(f'{w} - добавленно в список стоп слов')
            await asyncio.sleep(1)
        except sqlite3.IntegrityError as e:
            if 'UNIQUE constraint failed' in str(e):
                processed_words.append(f'❌ {w} - Уже есть в списке')
                continue

    words_success = "\n".join(processed_words)
    text = f'{hpre(words_success)}'
    await m.reply(text)

    global filter_worlds
    filter_worlds = [word[1] for word in db.select_all_word()]


@logger.catch
@dp.message_handler(IsAdmin(), commands=['stop_del'])
@dp.message_handler(AdminFilter(), commands=['stop_del'])
async def stop_delete(m: types.Message):
    words = await collect_words(m)
    processed_words = []

    for w in words:
        try:
            db.delete_word(w)
            processed_words.append(f"✅ {w} - удалено из списка стоп слов")
            logger.success(f'{w} - удалено из списка стоп слов')
            await asyncio.sleep(1)
        except Exception as e:
            print(e)
            processed_words.append(f'❌ {w} - Ошибка')
            # logger.error(f'❌ {w} - Ошибка')
            continue

    words_success = "\n".join(processed_words)
    text = f'{hpre(words_success)}'
    await m.reply(text)

    global filter_worlds
    filter_worlds = [word[1] for word in db.select_all_word()]


@logger.catch
@dp.message_handler(IsAdmin(), Command('stop_list'))
@dp.message_handler(AdminFilter(), Command('stop_list'))
async def list_stop_worlds(m: types.Message):
    messages = []
    temp_text = ''
    for i in filter_worlds:
        temp_text += f"{i}\n"
        if len(temp_text) > 4000:
            messages.append(temp_text)
            temp_text = ''
        if i is filter_worlds[-1]:
            messages.append(temp_text)

    for list_words in messages:
        no_words = 'Сюда пока что ничего не добавили((\nНапишите <code>/stop_add </code>' \
                   '[слово] - чтобы добавить слово в фильтр\n'
        text = f'Список всех стоп слов: \n{hpre(list_words)}' if len(filter_worlds) > 0 else no_words
        if len(messages) > 1 and list_words not in messages[0]:
            text = hpre(list_words)
        await m.answer(text)


@dp.message_handler(AdminFilter())
async def any_admin_message(m: types.Message):
    # await m.answer('u are admin in chat')
    pass


@dp.message_handler()
# @logger.catch
async def any_message(m: types.Message):
    global chats
    chat_id = m.chat.id
    user_id = m.from_user.id

    if chat_id in chats:
        result = await check_subs(m)
        if result == 1:
            keyboard = await subs_keys(chat_id, user_id)
            text = f'{hlink(m.from_user.full_name, f"tg://user?id={m.from_user.id}")},\n' \
                   f'❗️ Вы не подписались на каналы, поэтому Ваше сообщение удаляется.\n' \
                   f'✅ Подпишитесь на каналы, указанные ниже и сможете писать в чат.\n'

            warn = await m.answer(text, reply_markup=keyboard)
            try:
                await m.delete()
            except MessageToDeleteNotFound:
                pass
            await asyncio.sleep(60)
            await warn.delete()
            # try:
            # except MessageToDeleteNotFound:
            #     logger.error(f'Cant delete {warn}\n{traceback.extract_tb()}')
            # return

    answers = f"Добрый день, {hlink(m.from_user.full_name, f'tg://user?id={m.from_user.id}')}\n\n" \
              f"⚠️ Если Ваше объявление удаляется в чате, то Вы можете разместить его " \
              f"только на {hunderline('ПЛАТНОЙ ОСНОВЕ.')}\n" \
              f"С тарифами на размещение можно ознакомиться в боте: @Vacansy_resume_bot\n\n" \
              f"Администрация чата не несёт ответственности за размещаемую пользователями информацию и не исправляет её."
    for w in filter_worlds:
        if w.lower() in m.text.lower():
            mess = await m.answer(answers)
            try:
                await m.delete()
            except MessageToDeleteNotFound:
                pass
            await asyncio.sleep(10)
            try:
                logger.info(f'[delete] {m.chat.title[:20]} | {m.from_user.full_name[:20]}: {w[:20]}')
                await mess.delete()
            except MessageToDeleteNotFound:
                pass
            break


async def default_command(dp):
    for admin_id in config.ADMIN_ID:
        await dp.bot.set_my_commands([
            BotCommand('help', 'показать все комманды'),
            BotCommand('stop_add', 'Напиши после команды слово которое ловить и удалять'),
            BotCommand('stop_del', 'Напиши после команды слово которое хочешь удалить из базы'),
            BotCommand('stop_list', 'Показать все стоп-слова')
        ], BotCommandScopeChat(chat_id=admin_id))
    await dp.bot.set_my_commands([
        BotCommand('help', 'показать все комманды'),
        BotCommand('channel_add', 'Добавить канал в обязательный к подписке'),
        BotCommand('channel_del', 'Удалить канал из обязательных к подписке'),
        BotCommand('channel_list', 'Показать каналы этого чата'),
        BotCommand('stop_add', 'Напиши после команды слово которое ловить и удалять'),
        BotCommand('stop_del', 'Напиши после команды слово которое хочешь удалить из базы'),
        BotCommand('stop_list', 'Показать все стоп-слова'),
    ], BotCommandScopeAllChatAdministrators())


async def startup(dp):
    global chats
    chats = await update_chat_list()
    me = await dp.bot.get_me()
    await default_command(dp)
    logger.debug(f"bot started | {me.first_name} @{me.username}")


async def shut(dp):
    logger.debug("bot stopped")


if __name__ == '__main__':
    dp.middleware.setup(JoinUsers())
    # dp.middleware.setup(CheckSubscribe())
    dp.filters_factory.bind(IsAdmin)
    executor.start_polling(dp, on_startup=startup, on_shutdown=shut)
